# Copyright 2018 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

"""Server fixture for nginx."""


import argparse
import os
from shutil import copy
import signal
import subprocess
from textwrap import dedent

import fixtures
import tempita
from testtools.content import Content
from testtools.content_type import UTF8_TEXT

from maastesting.fixtures import TempDirectory
from provisioningserver.utils.fs import atomic_write

GENERATED_HEADER = dedent(
    """\
    # This is a file generated by the nginxfixture.
    # The nginxfixture tries not to overwrite existing configuration files
    # so it's safe to edit this file if you need to but be aware that
    # these changes won't be persisted.
    """
)


def preexec_fn():
    # Revert Python's handling of SIGPIPE. See
    # http://bugs.python.org/issue1652 for more info.
    signal.signal(signal.SIGPIPE, signal.SIG_DFL)


def should_write(path, overwrite_config=False):
    """Does the DNS config file at `path` need writing?

    :param path: File that may need to be written out.
    :param overwrite_config: Overwrite config files even if they
        already exist?
    :return: Whether the file should be written.
    :rtype: bool
    """
    return overwrite_config or not os.path.exists(path)


class NginxServerResources(fixtures.Fixture):
    """Allocate the resources a Nginx server needs.

    :ivar homedir: A directory where to put all the files the
        Nginx server needs (configuration files and executable).
    :ivar log_file: The log file allocated for the server.
    """

    # The full path where the 'nginx' executable can be found.
    # Note that it will be copied over to a temporary
    # location in order to by-pass the limitations imposed by
    # apparmor if the executable is in /usr/sbin/named.
    NGINX_PATH = "/usr/sbin/nginx"

    # The configuration template for the Nginx server.  The goal here
    # is to override the defaults (default configuration files location,
    # default port) to avoid clashing with the system's BIND (if
    # running).
    NGINX_CONF_TEMPLATE = tempita.Template(
        dedent(
            """
      pid {{pid_file}};
      worker_processes auto;

      error_log {{error_log_file}};

      events {
        worker_connections 768;
      }

      http {
        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 65;
        types_hash_max_size 2048;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        access_log {{access_log_file}};

        gzip on;

        include {{homedir}}/*.nginx.conf;
      }
    """
        )
    )

    def __init__(
        self, homedir=None, access_log_file=None, error_log_file=None
    ):
        super().__init__()
        self._defaults = dict(
            homedir=homedir,
            access_log_file=access_log_file,
            error_log_file=error_log_file,
        )

    def setUp(self, overwrite_config=False):
        super().setUp()
        self.__dict__.update(self._defaults)
        self.set_up_config()
        self.set_up_named(overwrite_config=overwrite_config)

    def set_up_named(self, overwrite_config=True):
        """Setup an environment to run 'nginx'.

        - Creates the default configuration for 'nginx'.
        - Copies the 'nginx' executable inside homedir.  AppArmor won't
          let us run the installed version the way we want.
        """
        # Write main Nginx config file.
        if should_write(self.conf_file, overwrite_config):
            nginx_conf = self.NGINX_CONF_TEMPLATE.substitute(
                homedir=self.homedir,
                access_log_file=self.access_log_file,
                error_log_file=self.error_log_file,
                pid_file=self.pid_file,
            )
            atomic_write(
                (GENERATED_HEADER + nginx_conf).encode("ascii"), self.conf_file
            )

        # Copy named executable to home dir.  This is done to avoid
        # the limitations imposed by apparmor if the executable
        # is in /usr/sbin/nginx.
        if should_write(self.nginx_file, overwrite_config):
            nginx_path = self.NGINX_PATH
            assert os.path.exists(nginx_path), (
                "'%s' executable not found.  Install the package "
                "'nginx-core' or define an environment variable named "
                "NGINX_PATH with the path where the 'nginx-core' "
                "executable can be found." % nginx_path
            )
            copy(nginx_path, self.nginx_file)

    def set_up_config(self):
        if self.homedir is None:
            self.homedir = self.useFixture(TempDirectory()).path
        if self.access_log_file is None:
            self.access_log_file = os.path.join(
                self.homedir, "nginx.access.log"
            )
        if self.error_log_file is None:
            self.error_log_file = os.path.join(self.homedir, "nginx.error.log")
        self.nginx_file = os.path.join(
            self.homedir, os.path.basename(self.NGINX_PATH)
        )
        self.conf_file = os.path.join(self.homedir, "nginx.conf")
        self.pid_file = os.path.join(self.homedir, "nginx.pid")


class NginxServerRunner(fixtures.Fixture):
    """Run a Nginx server."""

    def __init__(self, config):
        """Create a `NginxServerRunner` instance.

        :param config: An object exporting the variables
            `NginxServerResources` exports.
        """
        super().__init__()
        self.config = config
        self.process = None

    def setUp(self):
        super().setUp()
        self._start()

    def _start(self):
        """Spawn the Nginx server process."""
        env = dict(os.environ, HOME=self.config.homedir)
        with open(self.config.access_log_file, "wb") as log_file:
            with open(os.devnull, "rb") as devnull:
                self.process = subprocess.Popen(
                    [
                        self.config.nginx_file,
                        "-g",
                        "daemon off;",
                        "-c",
                        self.config.conf_file,
                    ],
                    stdin=devnull,
                    stdout=log_file,
                    stderr=log_file,
                    close_fds=True,
                    cwd=self.config.homedir,
                    env=env,
                    preexec_fn=preexec_fn,
                )
        self.addCleanup(self._stop)
        # Keep the log_file open for reading so that we can still get the log
        # even if the log is deleted.
        open_log_file = open(self.config.access_log_file, "rb")
        self.addDetail(
            os.path.basename(self.config.access_log_file),
            Content(UTF8_TEXT, lambda: open_log_file),
        )

    def _stop(self):
        """Stop the running server. Normally called by cleanups."""
        self.process.terminate()
        self.process.wait()


class NginxServer(fixtures.Fixture):
    """A Nginx server fixture.

    When setup a Nginx instance will be running.

    :ivar config: The `NginxServerResources` used to start the server.
    """

    def __init__(self, config=None):
        super().__init__()
        self.config = config

    def setUp(self):
        super().setUp()
        if self.config is None:
            self.config = NginxServerResources()
        self.useFixture(self.config)
        self.runner = NginxServerRunner(self.config)
        self.useFixture(self.runner)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Run a Nginx server.")
    parser.add_argument(
        "--homedir",
        help=(
            "A directory where to put all the files the Nginx"
            "server needs (configuration files and executable)"
        ),
    )
    parser.add_argument(
        "--access-log-file",
        help="The access log file allocated for the server",
    )
    parser.add_argument(
        "--error-log-file", help="The error log file allocated for the server"
    )
    parser.add_argument(
        "--overwrite-config",
        action="store_true",
        help="Whether or not to overwrite the configuration files "
        "if they already exist",
        default=False,
    )
    parser.add_argument(
        "--create-config-only",
        action="store_true",
        help="If set, only create the config files instead of "
        "also running the service [default: %(default)s].",
        default=False,
    )
    arguments = parser.parse_args()

    os.makedirs(arguments.homedir, exist_ok=True)

    # Create NginxServerResources with the provided options.
    resources = NginxServerResources(
        homedir=arguments.homedir,
        access_log_file=arguments.access_log_file,
        error_log_file=arguments.error_log_file,
    )
    resources.setUp(overwrite_config=arguments.overwrite_config)
    # Execute nginx.
    if not arguments.create_config_only:
        os.execlp(
            resources.nginx_file,
            resources.nginx_file,
            "-g",
            "daemon off;",
            "-c",
            resources.conf_file,
        )
